// Copyright 2016 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'package:collection/collection.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';

import 'ast/css.dart';
import 'ast/node.dart';
import 'ast/sass.dart';
import 'callable.dart';
import 'configuration.dart';
import 'configured_value.dart';
import 'exception.dart';
import 'extend/extension_store.dart';
import 'module.dart';
import 'module/forwarded_view.dart';
import 'module/shadowed_view.dart';
import 'util/map.dart';
import 'util/merged_map_view.dart';
import 'util/nullable.dart';
import 'util/public_member_map_view.dart';
import 'utils.dart';
import 'value.dart';
import 'visitor/clone_css.dart';

// TODO(nweiz): This used to avoid tracking source spans for variables if source
// map generation was disabled. We always have to track them now to produce
// better warnings for /-as-division, but once those warnings are gone we should
// go back to tracking conditionally.

/// The lexical environment in which Sass is executed.
///
/// This tracks lexically-scoped information, such as variables, functions, and
/// mixins.
final class AsyncEnvironment {
  /// The modules used in the current scope, indexed by their namespaces.
  Map<String, Module> get modules => UnmodifiableMapView(_modules);
  final Map<String, Module> _modules;

  /// A map from module namespaces to the nodes whose spans indicate where those
  /// modules were originally loaded.
  final Map<String, AstNode> _namespaceNodes;

  /// A map from namespaceless modules to the `@use` rules whose spans indicate
  /// where those modules were originally loaded.
  ///
  /// This does not include modules that were imported into the current scope.
  final Map<Module, AstNode> _globalModules;

  /// A map from modules that were imported into the current scope to the nodes
  /// whose spans indicate where those modules were originally loaded.
  final Map<Module, AstNode> _importedModules;

  /// A map from modules forwarded by this module to the nodes whose spans
  /// indicate where those modules were originally forwarded.
  ///
  /// This is `null` if there are no forwarded modules.
  Map<Module, AstNode>? _forwardedModules;

  /// Modules forwarded by nested imports at each lexical scope level *beneath
  /// the global scope*.
  ///
  /// This is `null` until it's needed, since most environments won't ever use
  /// this.
  List<List<Module>>? _nestedForwardedModules;

  /// Modules from [_modules], [_globalModules], and [_forwardedModules], in the
  /// order in which they were `@use`d.
  final List<Module> _allModules;

  /// A list of variables defined at each lexical scope level.
  ///
  /// Each scope maps the names of declared variables to their values.
  ///
  /// The first element is the global scope, and each successive element is
  /// deeper in the tree.
  final List<Map<String, Value>> _variables;

  /// The nodes where each variable in [_variables] was defined.
  ///
  /// This stores [AstNode]s rather than [FileSpan]s so it can avoid calling
  /// [AstNode.span] if the span isn't required, since some nodes need to do
  /// real work to manufacture a source span.
  final List<Map<String, AstNode>> _variableNodes;

  /// A map of variable names to their indices in [_variables].
  ///
  /// This map is filled in as-needed, and may not be complete.
  final Map<String, int> _variableIndices;

  /// A list of functions defined at each lexical scope level.
  ///
  /// Each scope maps the names of declared functions to their values.
  ///
  /// The first element is the global scope, and each successive element is
  /// deeper in the tree.
  final List<Map<String, AsyncCallable>> _functions;

  /// A map of function names to their indices in [_functions].
  ///
  /// This map is filled in as-needed, and may not be complete.
  final Map<String, int> _functionIndices;

  /// A list of mixins defined at each lexical scope level.
  ///
  /// Each scope maps the names of declared mixins to their values.
  ///
  /// The first element is the global scope, and each successive element is
  /// deeper in the tree.
  final List<Map<String, AsyncCallable>> _mixins;

  /// A map of mixin names to their indices in [_mixins].
  ///
  /// This map is filled in as-needed, and may not be complete.
  final Map<String, int> _mixinIndices;

  /// The content block passed to the lexically-enclosing mixin, or `null` if
  /// this is not in a mixin, or if no content block was passed.
  UserDefinedCallable<AsyncEnvironment>? get content => _content;
  UserDefinedCallable<AsyncEnvironment>? _content;

  /// The set of variable names that could be configured when loading the
  /// module.
  final Set<String> _configurableVariables;

  /// Whether the environment is lexically at the root of the document.
  bool get atRoot => _variables.length == 1;

  /// Whether the environment is lexically within a mixin.
  bool get inMixin => _inMixin;
  var _inMixin = false;

  /// Whether the environment is currently in a global or semi-global scope.
  ///
  /// A semi-global scope can assign to global variables, but it doesn't declare
  /// them by default.
  var _inSemiGlobalScope = true;

  /// The name of the last variable that was accessed.
  ///
  /// This is cached to speed up repeated references to the same variable, as
  /// well as references to the last variable's [FileSpan].
  String? _lastVariableName;

  /// The index in [_variables] of the last variable that was accessed.
  int? _lastVariableIndex;

  /// Creates an [AsyncEnvironment].
  ///
  /// If [sourceMap] is `true`, this tracks variables' source locations
  AsyncEnvironment()
      : _modules = {},
        _namespaceNodes = {},
        _globalModules = {},
        _importedModules = {},
        _forwardedModules = null,
        _nestedForwardedModules = null,
        _allModules = [],
        _variables = [{}],
        _variableNodes = [{}],
        _variableIndices = {},
        _functions = [{}],
        _functionIndices = {},
        _mixins = [{}],
        _mixinIndices = {},
        _configurableVariables = {};

  AsyncEnvironment._(
      this._modules,
      this._namespaceNodes,
      this._globalModules,
      this._importedModules,
      this._forwardedModules,
      this._nestedForwardedModules,
      this._allModules,
      this._variables,
      this._variableNodes,
      this._functions,
      this._mixins,
      this._content,
      this._configurableVariables)
      // Lazily fill in the indices rather than eagerly copying them from the
      // existing environment in closure() because the copying took a lot of
      // time and was rarely helpful. This saves a bunch of time on Susy's
      // tests.
      : _variableIndices = {},
        _functionIndices = {},
        _mixinIndices = {};

  /// Creates a closure based on this environment.
  ///
  /// Any scope changes in this environment will not affect the closure.
  /// However, any new declarations or assignments in scopes that are visible
  /// when the closure was created will be reflected.
  AsyncEnvironment closure() => AsyncEnvironment._(
        _modules,
        _namespaceNodes,
        _globalModules,
        _importedModules,
        _forwardedModules,
        _nestedForwardedModules,
        _allModules,
        _variables.toList(),
        _variableNodes.toList(),
        _functions.toList(),
        _mixins.toList(),
        _content,
        // Closures are always in nested contexts where configurable variables
        // are never added.
        const {},
      );

  /// Returns a new environment to use for an imported file.
  ///
  /// The returned environment shares this environment's variables, functions,
  /// and mixins, but excludes most modules (except for global modules that
  /// result from importing a file with forwards).
  AsyncEnvironment forImport() => AsyncEnvironment._(
        {},
        {},
        {},
        _importedModules,
        null,
        null,
        [],
        _variables.toList(),
        _variableNodes.toList(),
        _functions.toList(),
        _mixins.toList(),
        _content,
        _configurableVariables,
      );

  /// Adds [module] to the set of modules visible in this environment.
  ///
  /// [nodeWithSpan]'s span is used to report any errors with the module.
  ///
  /// If [namespace] is passed, the module is made available under that
  /// namespace.
  ///
  /// Throws a [SassScriptException] if there's already a module with the given
  /// [namespace], or if [namespace] is `null` and [module] defines a variable
  /// with the same name as a variable defined in this environment.
  void addModule(Module module, AstNode nodeWithSpan, {String? namespace}) {
    if (namespace == null) {
      _globalModules[module] = nodeWithSpan;
      _allModules.add(module);

      if (_variables.first.keys.firstWhereOrNull(module.variables.containsKey)
          case var name?) {
        throw SassScriptException(
          'This module and the new module both define a variable named '
          '"\$$name".',
        );
      }
    } else {
      if (_modules.containsKey(namespace)) {
        var span = _namespaceNodes[namespace]?.span;
        throw MultiSpanSassScriptException(
          "There's already a module with namespace \"$namespace\".",
          "new @use",
          {if (span != null) span: "original @use"},
        );
      }

      _modules[namespace] = module;
      _namespaceNodes[namespace] = nodeWithSpan;
      _allModules.add(module);
    }
  }

  /// Exposes the members in [module] to downstream modules as though they were
  /// defined in this module, according to the modifications defined by [rule].
  void forwardModule(Module module, ForwardRule rule) {
    var forwardedModules = (_forwardedModules ??= {});

    var view = ForwardedModuleView.ifNecessary(module, rule);
    for (var other in forwardedModules.keys) {
      _assertNoConflicts(
        view.variables,
        other.variables,
        view,
        other,
        "variable",
      );
      _assertNoConflicts(
        view.functions,
        other.functions,
        view,
        other,
        "function",
      );
      _assertNoConflicts(view.mixins, other.mixins, view, other, "mixin");
    }

    // Add the original module to [_allModules] (rather than the
    // [ForwardedModuleView]) so that we can de-duplicate upstream modules using
    // `==`. This is safe because upstream modules are only used for collating
    // CSS, not for the members they expose.
    _allModules.add(module);
    forwardedModules[view] = rule;
  }

  /// Throws a [SassScriptException] if [newMembers] from [newModule] has any
  /// keys that overlap with [oldMembers] from [oldModule].
  ///
  /// The [type] and [newModuleNodeWithSpan] are used for error reporting.
  void _assertNoConflicts(
    Map<String, Object> newMembers,
    Map<String, Object> oldMembers,
    Module newModule,
    Module oldModule,
    String type,
  ) {
    Map<String, Object> smaller;
    Map<String, Object> larger;
    if (newMembers.length < oldMembers.length) {
      smaller = newMembers;
      larger = oldMembers;
    } else {
      smaller = oldMembers;
      larger = newMembers;
    }

    for (var (name, small) in smaller.pairs) {
      var large = larger[name];
      if (large == null) continue;
      if (type == "variable"
          ? newModule.variableIdentity(name) == oldModule.variableIdentity(name)
          : large == small) {
        continue;
      }

      if (type == "variable") name = "\$$name";
      var span = _forwardedModules?[oldModule]?.span;
      throw MultiSpanSassScriptException(
        'Two forwarded modules both define a $type named $name.',
        "new @forward",
        {if (span != null) span: "original @forward"},
      );
    }
  }

  /// Makes the members forwarded by [module] available in the current
  /// environment.
  ///
  /// This is called when [module] is `@import`ed.
  void importForwards(Module module) {
    if (module is! _EnvironmentModule) return;
    var forwarded = module._environment._forwardedModules;
    if (forwarded == null) return;

    // Omit modules from [forwarded] that are already globally available and
    // forwarded in this module.
    var forwardedModules = _forwardedModules;
    if (forwardedModules != null) {
      forwarded = {
        for (var (module, node) in forwarded.pairs)
          if (!forwardedModules.containsKey(module) ||
              !_globalModules.containsKey(module))
            module: node,
      };
    } else {
      forwardedModules = _forwardedModules ??= {};
    }

    var forwardedVariableNames = {
      for (var module in forwarded.keys) ...module.variables.keys,
    };
    var forwardedFunctionNames = {
      for (var module in forwarded.keys) ...module.functions.keys,
    };
    var forwardedMixinNames = {
      for (var module in forwarded.keys) ...module.mixins.keys,
    };

    if (atRoot) {
      // Hide members from modules that have already been imported or
      // forwarded that would otherwise conflict with the @imported members.
      for (var (module, node) in _importedModules.pairs.toList()) {
        var shadowed = ShadowedModuleView.ifNecessary(
          module,
          variables: forwardedVariableNames,
          mixins: forwardedMixinNames,
          functions: forwardedFunctionNames,
        );
        if (shadowed != null) {
          _importedModules.remove(module);
          if (!shadowed.isEmpty) _importedModules[shadowed] = node;
        }
      }

      for (var (module, node) in forwardedModules.pairs.toList()) {
        var shadowed = ShadowedModuleView.ifNecessary(
          module,
          variables: forwardedVariableNames,
          mixins: forwardedMixinNames,
          functions: forwardedFunctionNames,
        );
        if (shadowed != null) {
          forwardedModules.remove(module);
          if (!shadowed.isEmpty) forwardedModules[shadowed] = node;
        }
      }

      _importedModules.addAll(forwarded);
      forwardedModules.addAll(forwarded);
    } else {
      (_nestedForwardedModules ??= List.generate(
        _variables.length - 1,
        (_) => [],
      ))
          .last
          .addAll(forwarded.keys);
    }

    // Remove existing member definitions that are now shadowed by the
    // forwarded modules.
    for (var variable in forwardedVariableNames) {
      _variableIndices.remove(variable);
      _variables.last.remove(variable);
      _variableNodes.last.remove(variable);
    }
    for (var function in forwardedFunctionNames) {
      _functionIndices.remove(function);
      _functions.last.remove(function);
    }
    for (var mixin in forwardedMixinNames) {
      _mixinIndices.remove(mixin);
      _mixins.last.remove(mixin);
    }
  }

  /// Returns the value of the variable named [name], optionally with the given
  /// [namespace], or `null` if no such variable is declared.
  ///
  /// Throws a [SassScriptException] if there is no module named [namespace], or
  /// if multiple global modules expose variables named [name].
  Value? getVariable(String name, {String? namespace}) {
    if (namespace != null) return _getModule(namespace).variables[name];

    if (_lastVariableName == name) {
      return _variables[_lastVariableIndex!][name] ??
          _getVariableFromGlobalModule(name);
    }

    if (_variableIndices[name] case var index?) {
      _lastVariableName = name;
      _lastVariableIndex = index;
      return _variables[index][name] ?? _getVariableFromGlobalModule(name);
    } else if (_variableIndex(name) case var index?) {
      _lastVariableName = name;
      _lastVariableIndex = index;
      _variableIndices[name] = index;
      return _variables[index][name] ?? _getVariableFromGlobalModule(name);
    } else {
      // There isn't a real variable defined as this index, but it will cause
      // [getVariable] to short-circuit and get to this function faster next
      // time the variable is accessed.
      return _getVariableFromGlobalModule(name);
    }
  }

  /// Returns the value of the variable named [name] from a namespaceless
  /// module, or `null` if no such variable is declared in any namespaceless
  /// module.
  Value? _getVariableFromGlobalModule(String name) =>
      _fromOneModule(name, "variable", (module) => module.variables[name]);

  /// Returns the node for the variable named [name], or `null` if no such
  /// variable is declared.
  ///
  /// This node is intended as a proxy for the [FileSpan] indicating where the
  /// variable's value originated. It's returned as an [AstNode] rather than a
  /// [FileSpan] so we can avoid calling [AstNode.span] if the span isn't
  /// required, since some nodes need to do real work to manufacture a source
  /// span.
  AstNode? getVariableNode(String name, {String? namespace}) {
    if (namespace != null) return _getModule(namespace).variableNodes[name];

    if (_lastVariableName == name) {
      return _variableNodes[_lastVariableIndex!][name] ??
          _getVariableNodeFromGlobalModule(name);
    }

    if (_variableIndices[name] case var index?) {
      _lastVariableName = name;
      _lastVariableIndex = index;
      return _variableNodes[index][name] ??
          _getVariableNodeFromGlobalModule(name);
    } else if (_variableIndex(name) case var index?) {
      _lastVariableName = name;
      _lastVariableIndex = index;
      _variableIndices[name] = index;
      return _variableNodes[index][name] ??
          _getVariableNodeFromGlobalModule(name);
    } else {
      return _getVariableNodeFromGlobalModule(name);
    }
  }

  /// Returns the node for the variable named [name] from a namespaceless
  /// module, or `null` if no such variable is declared.
  ///
  /// This node is intended as a proxy for the [FileSpan] indicating where the
  /// variable's value originated. It's returned as an [AstNode] rather than a
  /// [FileSpan] so we can avoid calling [AstNode.span] if the span isn't
  /// required, since some nodes need to do real work to manufacture a source
  /// span.
  AstNode? _getVariableNodeFromGlobalModule(String name) {
    // We don't need to worry about multiple modules defining the same variable,
    // because that's already been checked by [getVariable].
    for (var module in _importedModules.keys.followedBy(_globalModules.keys)) {
      if (module.variableNodes[name] case var value?) return value;
    }
    return null;
  }

  /// Returns whether a variable named [name] exists.
  bool variableExists(String name) => getVariable(name) != null;

  /// Returns whether a global variable named [name] exists.
  ///
  /// Throws a [SassScriptException] if there is no module named [namespace], or
  /// if multiple global modules expose functions named [name].
  bool globalVariableExists(String name, {String? namespace}) {
    if (namespace != null) {
      return _getModule(namespace).variables.containsKey(name);
    }
    if (_variables.first.containsKey(name)) return true;
    return _getVariableFromGlobalModule(name) != null;
  }

  /// Returns the index of the last map in [_variables] that has a [name] key,
  /// or `null` if none exists.
  int? _variableIndex(String name) {
    for (var i = _variables.length - 1; i >= 0; i--) {
      if (_variables[i].containsKey(name)) return i;
    }
    return null;
  }

  /// Sets the variable named [name] to [value], associated with
  /// [nodeWithSpan]'s source span.
  ///
  /// If [namespace] is passed, this sets the variable in the module with the
  /// given namespace, if that module exposes a variable with that name.
  ///
  /// If [global] is `true`, this sets the variable at the top-level scope.
  /// Otherwise, if the variable was already defined, it'll set it in the
  /// previous scope. If it's undefined, it'll set it in the current scope.
  ///
  /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling
  /// [AstNode.span] if the span isn't required, since some nodes need to do
  /// real work to manufacture a source span.
  ///
  /// Throws a [SassScriptException] if [namespace] is passed but no module is
  /// defined with the given namespace, if no variable with the given [name] is
  /// defined in module with the given namespace, or if no [namespace] is passed
  /// and multiple global modules define variables named [name].
  void setVariable(
    String name,
    Value value,
    AstNode nodeWithSpan, {
    String? namespace,
    bool global = false,
  }) {
    if (namespace != null) {
      _getModule(namespace).setVariable(name, value, nodeWithSpan);
      return;
    }

    if (global || atRoot) {
      // Don't set the index if there's already a variable with the given name,
      // since local accesses should still return the local variable.
      _variableIndices.putIfAbsent(name, () {
        _lastVariableName = name;
        _lastVariableIndex = 0;
        return 0;
      });

      // If this module doesn't already contain a variable named [name], try
      // setting it in a global module.
      if (!_variables.first.containsKey(name)) {
        var moduleWithName = _fromOneModule(
          name,
          "variable",
          (module) => module.variables.containsKey(name) ? module : null,
        );
        if (moduleWithName != null) {
          moduleWithName.setVariable(name, value, nodeWithSpan);
          return;
        }
      }

      _variables.first[name] = value;
      _variableNodes.first[name] = nodeWithSpan;
      return;
    }

    var nestedForwardedModules = _nestedForwardedModules;
    if (nestedForwardedModules != null &&
        !_variableIndices.containsKey(name) &&
        _variableIndex(name) == null) {
      for (var modules in nestedForwardedModules.reversed) {
        for (var module in modules.reversed) {
          if (module.variables.containsKey(name)) {
            module.setVariable(name, value, nodeWithSpan);
            return;
          }
        }
      }
    }

    var index = _lastVariableName == name
        ? _lastVariableIndex!
        : _variableIndices.putIfAbsent(
            name,
            () => _variableIndex(name) ?? _variables.length - 1,
          );
    if (!_inSemiGlobalScope && index == 0) {
      index = _variables.length - 1;
      _variableIndices[name] = index;
    }

    _lastVariableName = name;
    _lastVariableIndex = index;
    _variables[index][name] = value;
    _variableNodes[index][name] = nodeWithSpan;
  }

  /// Sets the variable named [name] to [value], associated with
  /// [nodeWithSpan]'s source span.
  ///
  /// Unlike [setVariable], this will declare the variable in the current scope
  /// even if a declaration already exists in an outer scope.
  ///
  /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling
  /// [AstNode.span] if the span isn't required, since some nodes need to do
  /// real work to manufacture a source span.
  void setLocalVariable(String name, Value value, AstNode nodeWithSpan) {
    var index = _variables.length - 1;
    _lastVariableName = name;
    _lastVariableIndex = index;
    _variableIndices[name] = index;
    _variables[index][name] = value;
    _variableNodes[index][name] = nodeWithSpan;
  }

  /// Records that [name] is a variable that could have been configured for this
  /// module, whether or not it actually was.
  ///
  /// This is used to determine whether to throw an error when passing a new
  /// configuration through `@forward` to an already-loaded module.
  void markVariableConfigurable(String name) {
    _configurableVariables.add(name);
  }

  /// Returns the value of the function named [name], optionally with the given
  /// [namespace], or `null` if no such variable is declared.
  ///
  /// Throws a [SassScriptException] if there is no module named [namespace], or
  /// if multiple global modules expose functions named [name].
  AsyncCallable? getFunction(String name, {String? namespace}) {
    if (namespace != null) return _getModule(namespace).functions[name];

    if (_functionIndices[name] case var index?) {
      return _functions[index][name] ?? _getFunctionFromGlobalModule(name);
    } else if (_functionIndex(name) case var index?) {
      _functionIndices[name] = index;
      return _functions[index][name] ?? _getFunctionFromGlobalModule(name);
    } else {
      return _getFunctionFromGlobalModule(name);
    }
  }

  /// Returns the value of the function named [name] from a namespaceless
  /// module, or `null` if no such function is declared in any namespaceless
  /// module.
  AsyncCallable? _getFunctionFromGlobalModule(String name) =>
      _fromOneModule(name, "function", (module) => module.functions[name]);

  /// Returns the index of the last map in [_functions] that has a [name] key,
  /// or `null` if none exists.
  int? _functionIndex(String name) {
    for (var i = _functions.length - 1; i >= 0; i--) {
      if (_functions[i].containsKey(name)) return i;
    }
    return null;
  }

  /// Returns whether a function named [name] exists.
  ///
  /// Throws a [SassScriptException] if there is no module named [namespace], or
  /// if multiple global modules expose functions named [name].
  bool functionExists(String name, {String? namespace}) =>
      getFunction(name, namespace: namespace) != null;

  /// Sets the variable named [name] to [value] in the current scope.
  void setFunction(AsyncCallable callable) {
    var index = _functions.length - 1;
    _functionIndices[callable.name] = index;
    _functions[index][callable.name] = callable;
  }

  /// Returns the value of the mixin named [name], optionally with the given
  /// [namespace], or `null` if no such variable is declared.
  ///
  /// Throws a [SassScriptException] if there is no module named [namespace], or
  /// if multiple global modules expose mixins named [name].
  AsyncCallable? getMixin(String name, {String? namespace}) {
    if (namespace != null) return _getModule(namespace).mixins[name];

    if (_mixinIndices[name] case var index?) {
      return _mixins[index][name] ?? _getMixinFromGlobalModule(name);
    } else if (_mixinIndex(name) case var index?) {
      _mixinIndices[name] = index;
      return _mixins[index][name] ?? _getMixinFromGlobalModule(name);
    } else {
      return _getMixinFromGlobalModule(name);
    }
  }

  /// Returns the value of the mixin named [name] from a namespaceless
  /// module, or `null` if no such mixin is declared in any namespaceless
  /// module.
  AsyncCallable? _getMixinFromGlobalModule(String name) =>
      _fromOneModule(name, "mixin", (module) => module.mixins[name]);

  /// Returns the index of the last map in [_mixins] that has a [name] key, or
  /// `null` if none exists.
  int? _mixinIndex(String name) {
    for (var i = _mixins.length - 1; i >= 0; i--) {
      if (_mixins[i].containsKey(name)) return i;
    }
    return null;
  }

  /// Returns whether a mixin named [name] exists.
  ///
  /// Throws a [SassScriptException] if there is no module named [namespace], or
  /// if multiple global modules expose functions named [name].
  bool mixinExists(String name, {String? namespace}) =>
      getMixin(name, namespace: namespace) != null;

  /// Sets the variable named [name] to [value] in the current scope.
  void setMixin(AsyncCallable callable) {
    var index = _mixins.length - 1;
    _mixinIndices[callable.name] = index;
    _mixins[index][callable.name] = callable;
  }

  /// Sets [content] as [this.content] for the duration of [callback].
  Future<void> withContent(
    UserDefinedCallable<AsyncEnvironment>? content,
    Future<void> callback(),
  ) async {
    var oldContent = _content;
    _content = content;
    await callback();
    _content = oldContent;
  }

  /// Sets [inMixin] to `true` for the duration of [callback].
  Future<void> asMixin(Future<void> callback()) async {
    var oldInMixin = _inMixin;
    _inMixin = true;
    await callback();
    _inMixin = oldInMixin;
  }

  /// Runs [callback] in a new scope.
  ///
  /// Variables, functions, and mixins declared in a given scope are
  /// inaccessible outside of it. If [semiGlobal] is passed, this scope can
  /// assign to global variables without a `!global` declaration.
  ///
  /// If [when] is false, this doesn't create a new scope and instead just
  /// executes [callback] and returns its result.
  Future<T> scope<T>(
    Future<T> callback(), {
    bool semiGlobal = false,
    bool when = true,
  }) async {
    // We have to track semi-globalness even if `!when` so that
    //
    //     div {
    //       @if ... {
    //         $x: y;
    //       }
    //     }
    //
    // doesn't assign to the global scope.
    semiGlobal = semiGlobal && _inSemiGlobalScope;
    var wasInSemiGlobalScope = _inSemiGlobalScope;
    _inSemiGlobalScope = semiGlobal;

    if (!when) {
      try {
        return await callback();
      } finally {
        _inSemiGlobalScope = wasInSemiGlobalScope;
      }
    }

    _variables.add({});
    _variableNodes.add({});
    _functions.add({});
    _mixins.add({});
    _nestedForwardedModules?.add([]);
    try {
      return await callback();
    } finally {
      _inSemiGlobalScope = wasInSemiGlobalScope;
      _lastVariableName = null;
      _lastVariableIndex = null;
      for (var name in _variables.removeLast().keys) {
        _variableIndices.remove(name);
      }
      _variableNodes.removeLast();
      for (var name in _functions.removeLast().keys) {
        _functionIndices.remove(name);
      }
      for (var name in _mixins.removeLast().keys) {
        _mixinIndices.remove(name);
      }
      _nestedForwardedModules?.removeLast();
    }
  }

  /// Creates an implicit configuration from the variables declared in this
  /// environment.
  Configuration toImplicitConfiguration() {
    var configuration = <String, ConfiguredValue>{};
    for (var module in _importedModules.keys) {
      for (var (name, value) in module.variables.pairs) {
        configuration[name] =
            ConfiguredValue.implicit(value, module.variableNodes[name]!);
      }
    }
    for (var i = 0; i < _variables.length; i++) {
      var values = _variables[i];
      var nodes = _variableNodes[i];
      for (var (name, value) in values.pairs) {
        configuration[name] = ConfiguredValue.implicit(value, nodes[name]!);
      }
    }
    return Configuration.implicit(configuration);
  }

  /// Returns a module that represents the top-level members defined in `this`,
  /// that contains [css] and [preModuleComments] as its CSS, which can be
  /// extended using [extensionStore].
  Module toModule(
    CssStylesheet css,
    Map<Module, List<CssComment>> preModuleComments,
    ExtensionStore extensionStore,
  ) {
    assert(atRoot);
    return _EnvironmentModule(
      this,
      css,
      preModuleComments,
      extensionStore,
      forwarded: _forwardedModules.andThen((modules) => MapKeySet(modules)),
    );
  }

  /// Returns a module with the same members and upstream modules as `this`, but
  /// an empty stylesheet and extension store.
  ///
  /// This is used when resolving imports, since they need to inject forwarded
  /// members into the current scope. It's the only situation in which a nested
  /// environment can become a module.
  Module toDummyModule() => _EnvironmentModule(
        this,
        CssStylesheet(
          const [],
          SourceFile.decoded(const [], url: "<dummy module>").span(0),
        ),
        const {},
        ExtensionStore.empty,
        forwarded: _forwardedModules.andThen((modules) => MapKeySet(modules)),
      );

  /// Returns the module with the given [namespace], or throws a
  /// [SassScriptException] if none exists.
  Module _getModule(String namespace) {
    if (_modules[namespace] case var module?) return module;
    throw SassScriptException(
      'There is no module with the namespace "$namespace".',
    );
  }

  /// Returns the result of [callback] if it returns non-`null` for exactly one
  /// module in [_globalModules] *or* for any module in [_importedModules] or
  /// [_nestedForwardedModules].
  ///
  /// Returns `null` if [callback] returns `null` for all modules. Throws an
  /// error if [callback] returns non-`null` for more than one module.
  ///
  /// The [name] is the name of the member being looked up.
  ///
  /// The [type] should be the singular name of the value type being returned.
  /// It's used to format an appropriate error message.
  T? _fromOneModule<T>(String name, String type, T? callback(Module module)) {
    if (_nestedForwardedModules case var nestedForwardedModules?) {
      for (var modules in nestedForwardedModules.reversed) {
        for (var module in modules.reversed) {
          if (callback(module) case var value?) return value;
        }
      }
    }
    for (var module in _importedModules.keys) {
      if (callback(module) case var value?) return value;
    }

    T? value;
    Object? identity;
    for (var module in _globalModules.keys) {
      var valueInModule = callback(module);
      if (valueInModule == null) continue;

      Object? identityFromModule = valueInModule is AsyncCallable
          ? valueInModule
          : module.variableIdentity(name);
      if (identityFromModule == identity) continue;

      if (value != null) {
        throw MultiSpanSassScriptException(
          'This $type is available from multiple global modules.',
          '$type use',
          {
            for (var (module, node) in _globalModules.pairs)
              if (callback(module) != null) node.span: 'includes $type',
          },
        );
      }

      value = valueInModule;
      identity = identityFromModule;
    }
    return value;
  }
}

/// A module that represents the top-level members defined in an [Environment].
final class _EnvironmentModule implements Module {
  Uri? get url => css.span.sourceUrl;

  final List<Module> upstream;
  final Map<String, Value> variables;
  final Map<String, AstNode> variableNodes;
  final Map<String, AsyncCallable> functions;
  final Map<String, AsyncCallable> mixins;
  final ExtensionStore extensionStore;
  final CssStylesheet css;
  final Map<Module, List<CssComment>> preModuleComments;
  final bool transitivelyContainsCss;
  final bool transitivelyContainsExtensions;

  /// The environment that defines this module's members.
  final AsyncEnvironment _environment;

  /// A map from variable names to the modules in which those variables appear,
  /// used to determine where variables should be set.
  ///
  /// Variables that don't appear in this map are either defined directly in
  /// this module (if they appear in `_environment._variables.first`) or not
  /// defined at all.
  final Map<String, Module> _modulesByVariable;

  factory _EnvironmentModule(
    AsyncEnvironment environment,
    CssStylesheet css,
    Map<Module, List<CssComment>> preModuleComments,
    ExtensionStore extensionStore, {
    Set<Module>? forwarded,
  }) {
    forwarded ??= const {};
    return _EnvironmentModule._(
      environment,
      css,
      Map.unmodifiable({
        for (var (module, comments) in preModuleComments.pairs)
          module: List<CssComment>.unmodifiable(comments),
      }),
      extensionStore,
      _makeModulesByVariable(forwarded),
      _memberMap(
        environment._variables.first,
        forwarded.map((module) => module.variables),
      ),
      _memberMap(
        environment._variableNodes.first,
        forwarded.map((module) => module.variableNodes),
      ),
      _memberMap(
        environment._functions.first,
        forwarded.map((module) => module.functions),
      ),
      _memberMap(
        environment._mixins.first,
        forwarded.map((module) => module.mixins),
      ),
      transitivelyContainsCss: css.children.isNotEmpty ||
          preModuleComments.isNotEmpty ||
          environment._allModules.any(
            (module) => module.transitivelyContainsCss,
          ),
      transitivelyContainsExtensions: !extensionStore.isEmpty ||
          environment._allModules.any(
            (module) => module.transitivelyContainsExtensions,
          ),
    );
  }

  /// Create [_modulesByVariable] for a set of forwarded modules.
  static Map<String, Module> _makeModulesByVariable(Set<Module> forwarded) {
    if (forwarded.isEmpty) return const {};

    var modulesByVariable = <String, Module>{};
    for (var module in forwarded) {
      if (module is _EnvironmentModule) {
        // Flatten nested forwarded modules to avoid O(depth) overhead.
        for (var child in module._modulesByVariable.values) {
          setAll(modulesByVariable, child.variables.keys, child);
        }
        setAll(
          modulesByVariable,
          module._environment._variables.first.keys,
          module,
        );
      } else {
        setAll(modulesByVariable, module.variables.keys, module);
      }
    }
    return modulesByVariable;
  }

  /// Returns a map that exposes the public members of [localMap] as well as all
  /// the members of [otherMaps].
  static Map<String, V> _memberMap<V>(
    Map<String, V> localMap,
    Iterable<Map<String, V>> otherMaps,
  ) {
    localMap = PublicMemberMapView(localMap);
    if (otherMaps.isEmpty) return localMap;

    var allMaps = [
      for (var map in otherMaps)
        if (map.isNotEmpty) map,
      localMap,
    ];
    if (allMaps.length == 1) return localMap;

    return MergedMapView(allMaps);
  }

  _EnvironmentModule._(
    this._environment,
    this.css,
    this.preModuleComments,
    this.extensionStore,
    this._modulesByVariable,
    this.variables,
    this.variableNodes,
    this.functions,
    this.mixins, {
    required this.transitivelyContainsCss,
    required this.transitivelyContainsExtensions,
  }) : upstream = _environment._allModules;

  void setVariable(String name, Value value, AstNode nodeWithSpan) {
    if (_modulesByVariable[name] case var module?) {
      module.setVariable(name, value, nodeWithSpan);
      return;
    }

    if (!_environment._variables.first.containsKey(name)) {
      throw SassScriptException("Undefined variable.");
    }

    _environment._variables.first[name] = value;
    _environment._variableNodes.first[name] = nodeWithSpan;
    return;
  }

  Object variableIdentity(String name) {
    assert(variables.containsKey(name));
    var module = _modulesByVariable[name];
    return module == null ? this : module.variableIdentity(name);
  }

  bool couldHaveBeenConfigured(Set<String> variables) =>
      // Check if this module defines a configurable variable with any of the
      // given names.
      (variables.length < _environment._configurableVariables.length
          ? variables.any(_environment._configurableVariables.contains)
          : _environment._configurableVariables.any(variables.contains)) ||
      // Find forwarded modules whose variables overlap with [variables] and
      // check if they define configurable variables with any of the given
      // names.
      (variables.length < _modulesByVariable.length
              ? {
                  for (var variable in variables)
                    if (_modulesByVariable[variable] case var module?) module
                }
              : {
                  for (var (variable, module) in _modulesByVariable.pairs)
                    if (variables.contains(variable)) module
                })
          .any((module) => module.couldHaveBeenConfigured(variables));

  Module cloneCss() {
    if (!transitivelyContainsCss) return this;

    var (newStylesheet, newExtensionStore) = cloneCssStylesheet(
      css,
      extensionStore,
    );
    return _EnvironmentModule._(
      _environment,
      newStylesheet,
      preModuleComments,
      newExtensionStore,
      _modulesByVariable,
      variables,
      variableNodes,
      functions,
      mixins,
      transitivelyContainsCss: transitivelyContainsCss,
      transitivelyContainsExtensions: transitivelyContainsExtensions,
    );
  }

  String toString() => url == null ? "<unknown url>" : p.prettyUri(url);
}
